package org.luxor.common.netty.utils;


import com.fasterxml.jackson.databind.util.ObjectBuffer;

import java.io.UnsupportedEncodingException;
import java.nio.*;
import java.nio.charset.Charset;
import java.util.*;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.locks.LockSupport;

/**
 * 字节缓冲区工具类
 *
 * @author Mr.yan @date 2023/10/11
 */
public class BufferUtils {
    private static final byte[] EMPTY_BYTES = new byte[0];

    public static byte[] short2bytes(short value) {
        ByteBuffer buffer = ByteBuffer.allocate(2);
        buffer.putShort(value);
        buffer.flip();
        return readBytes(buffer);
    }

    public static byte[] int2bytes(int value) {
        ByteBuffer buffer = ByteBuffer.allocate(4);
        buffer.putInt(value);
        buffer.flip();
        return readBytes(buffer);
    }

    public static byte[] long2bytes(long value) {
        ByteBuffer buffer = ByteBuffer.allocate(8);
        buffer.putLong(value);
        buffer.flip();
        return readBytes(buffer);
    }

    public static byte[] float2bytes(float value) {
        ByteBuffer buffer = ByteBuffer.allocate(8);
        buffer.putFloat(value);
        buffer.flip();
        return readBytes(buffer);
    }

    public static byte[] double2bytes(double value) {
        ByteBuffer buffer = ByteBuffer.allocate(8);
        buffer.putDouble(value);
        buffer.flip();
        return readBytes(buffer);
    }

    public static byte[] str2bytes(String value) {
        if (value == null) {
            return null;
        }
        return value.getBytes();
    }

    public static byte[] str2bytes(String value, String encoding) {
        if (value == null) {
            return null;
        }
        try {
            return value.getBytes(encoding);
        } catch (UnsupportedEncodingException err) {
            err.printStackTrace();
            return value.getBytes();
        }
    }

    public static byte[] str2bytes(String value, Charset encoding) {
        if (value == null) {
            return null;
        }
        return value.getBytes(encoding);
    }

    public static byte[] str2bytes(String value, int length) {
        if (value == null || value.length() <= 0) {
            return new byte[length];
        }
        byte[] bts1 = value.getBytes();
        if (bts1.length == length) {
            return bts1;
        }
        byte[] bts2 = new byte[length];
        System.arraycopy(bts1, 0, bts2, 0, bts1.length > length ? length : bts1.length);
        return bts2;
    }

    public static byte[] str2bytes(String value, String encoding, int length) {
        if (value == null || value.length() <= 0) {
            return new byte[length];
        }
        byte[] bts1 = str2bytes(value, encoding);
        if (bts1.length == length) {
            return bts1;
        }
        byte[] bts2 = new byte[length];
        System.arraycopy(bts1, 0, bts2, 0, bts1.length > length ? length : bts1.length);
        return bts2;
    }

    public static byte[] str2bytes(String value, Charset encoding, int length) {
        if (value == null || value.length() <= 0) {
            return new byte[length];
        }
        byte[] bts1 = str2bytes(value, encoding);
        if (bts1.length == length) {
            return bts1;
        }
        byte[] bts2 = new byte[length];
        System.arraycopy(bts1, 0, bts2, 0, bts1.length > length ? length : bts1.length);
        return bts2;
    }

    public static byte[] short2bytes(int value) {
        byte[] bts = new byte[2];
        for (int i = bts.length - 1; i >= 0; i--) {
            bts[i] = (byte) (value & 0x000000ff);
            value >>>= 8;
        }
        return bts;
    }

    public static byte[] uint2bytes(long value) {
        byte[] bts = new byte[4];
        for (int i = bts.length - 1; i >= 0; i--) {
            bts[i] = (byte) (value & 0x000000ff);
            value >>>= 8;
        }
        return bts;
    }

    public static byte[] long2bytes(long value, boolean unsigned) {
        if (!unsigned) {
            return long2bytes(value);
        } else {
            byte[] bts = new byte[8];
            for (int i = bts.length - 1; i >= 0; i--) {
                bts[i] = (byte) (value & 0x000000ff);
                value >>>= 8;
            }
            return bts;
        }
    }

    public static short bytes2short(byte[] obj) {
        ByteBuffer buffer = ByteBuffer.wrap(obj);
        return buffer.getShort();
    }

    public static short bytes2short(byte[] obj, int from) {
        ByteBuffer buffer = ByteBuffer.wrap(obj, from, obj.length - from);
        return buffer.getShort();
    }

    public static int bytes2int(byte[] obj) {
        ByteBuffer buffer = ByteBuffer.wrap(obj);
        return buffer.getInt();
    }

    public static int bytes2int(byte[] obj, int from) {
        ByteBuffer buffer = ByteBuffer.wrap(obj, from, obj.length - from);
        return buffer.getInt();
    }

    public static long bytes2long(byte[] obj) {
        ByteBuffer buffer = ByteBuffer.wrap(obj);
        return buffer.getLong();
    }

    public static long bytes2long(byte[] obj, int from) {
        ByteBuffer buffer = ByteBuffer.wrap(obj, from, obj.length - from);
        return buffer.getLong();
    }

    public static float bytes2float(byte[] obj) {
        ByteBuffer buffer = ByteBuffer.wrap(obj);
        return buffer.getFloat();
    }

    public static float bytes2float(byte[] obj, int from) {
        ByteBuffer buffer = ByteBuffer.wrap(obj, from, obj.length - from);
        return buffer.getFloat();
    }

    public static double bytes2double(byte[] obj) {
        ByteBuffer buffer = ByteBuffer.wrap(obj);
        return buffer.getDouble();
    }

    public static double bytes2double(byte[] obj, int from) {
        ByteBuffer buffer = ByteBuffer.wrap(obj, from, obj.length - from);
        return buffer.getDouble();
    }

    public static int bytes2UnsignedShort(byte[] obj) {
        ByteBuffer buffer = ByteBuffer.wrap(obj);
        return readUnsignedShort(buffer);
    }

    public static int bytes2UnsignedShort(byte[] obj, int from) {
        ByteBuffer buffer = ByteBuffer.wrap(obj, from, obj.length - from);
        return readUnsignedShort(buffer);
    }

    public static long bytes2UnsignedInt(byte[] obj) {
        ByteBuffer buffer = ByteBuffer.wrap(obj);
        return readUnsignedInt(buffer);
    }

    public static long bytes2UnsignedInt(byte[] obj, int from) {
        ByteBuffer buffer = ByteBuffer.wrap(obj, from, obj.length - from);
        return readUnsignedInt(buffer);
    }

    public static String bytes2str(byte[] obj) {
        if (obj == null || obj.length <= 0) {
            return null;
        }

        int begin = 0;
        for (; begin < obj.length && obj[begin] == 0x0; begin++) {
        }

        int end = begin;
        for (; end < obj.length && obj[end] != 0x0; end++) {
        }

        return new String(obj, begin, end - begin);
    }

    public static String bytes2str(byte[] obj, String encoding) {
        if (obj == null || obj.length <= 0) {
            return null;
        }

        int begin = 0;
        for (; begin < obj.length && obj[begin] == 0x0; begin++) {
        }

        int end = begin;
        for (; end < obj.length && obj[end] != 0x0; end++) {
        }

        try {
            return new String(obj, begin, end - begin, encoding);
        } catch (UnsupportedEncodingException err) {
            err.printStackTrace();
            return new String(obj);
        }
    }

    public static String readString(ByteBuffer buffer, int length) {
        byte[] tmp = new byte[length < 0 ? buffer.remaining() : length];
        buffer.get(tmp);
        return bytes2str(tmp);
    }

    public static String readString(ByteBuffer buffer, String encoding, int length) {
        if (!buffer.hasRemaining()) {
            return null;
        }
        byte[] tmp = new byte[length < 0 ? buffer.remaining() : length];
        buffer.get(tmp);
        return bytes2str(tmp, encoding);
    }

    public static String readString(ByteBuffer buffer) {
        return readString(buffer, -1);
    }

    public static String readString(ByteBuffer buffer, String encoding) {
        return readString(buffer, encoding, -1);
    }

    public static byte[] readBytes(ByteBuffer buffer, int length) {
        if (!buffer.hasRemaining()) {
            return EMPTY_BYTES;
        }
        byte[] bts = new byte[length < 0 ? buffer.remaining() : length];
        buffer.get(bts);

        return bts;
    }

    public static void skipBytes(ByteBuffer buffer) {
        skipBytes(buffer, -1);
    }

    public static void skipBytes(ByteBuffer buffer, int length) {
        if (!buffer.hasRemaining()) {
            return;
        }
        buffer.position(buffer.position() + (length < 0 ? buffer.remaining() : length));
    }

    public static byte[] readBytes(ByteBuffer buffer) {
        return readBytes(buffer, -1);
    }

    public static byte[] getFixedLengthBytes(byte[] origin, int length) {
        if (origin == null) {
            return length == 0 ? null : new byte[length];
        }
        if (origin.length == length) {
            return origin;
        }
        byte[] bts = new byte[length];
        System.arraycopy(origin, 0, bts, 0, Math.min(origin.length, bts.length));
        return bts;
    }

    public static byte[] trim(byte[] _message) {
        byte[] validByte = null;
        if (null != _message && _message.length > 0) {
            int begin = 0;
            for (; begin < _message.length && _message[begin] == 0x0; begin++) {
            }

            int end = begin;
            for (; end < _message.length && _message[end] != 0x0; end++) {
            }

            if (begin == 0 && end >= _message.length) {
                return _message;
            } else {
                validByte = new byte[end - begin];
                System.arraycopy(_message, begin, validByte, 0, end - begin);
            }
        }
        return validByte;
    }

    public static int readUnsignedShort(ByteBuffer buffer) {
        byte[] bts = new byte[2];
        buffer.get(bts);

        int result = 0;
        for (int i = 0; i < bts.length; i++) {
            result <<= 8;
            result |= (bts[i] & 0x000000ff);
        }
        return result;
    }

    public static long readUnsignedInt(ByteBuffer buffer) {
        byte[] bts = new byte[4];
        buffer.get(bts);

        int result = 0;
        for (int i = 0; i < bts.length; i++) {
            result <<= 8;
            result |= (bts[i] & 0x000000ff);
        }
        return result;
    }


    //Allocator function
    public static int[] chunkSizes = new int[]{4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096, 8192, 16384, 32768, 65536,
            65536 + 32768, 65536 + 32768 * 2, 65536 + 32768 * 3, 65536 + 32768 * 4, 65536 + 32768 * 5, 65536 + 32768 * 6, 65536 + 32768 * 7, 65536 + 32768 * 8, 65536 + 32768 * 9,
            65536 + 32768 * 10, 65536 + 32768 * 11, 65536 + 32768 * 12, 65536 + 32768 * 13, 65536 + 32768 * 14, 65536 + 32768 * 15, 65536 + 32768 * 16,
            65536 + 32768 * 17, 65536 + 32768 * 18, 65536 + 32768 * 19, 65536 + 32768 * 20, 65536 + 32768 * 21, 65536 + 32768 * 22, 65536 + 32768 * 23, 65536 + 32768 * 24,
            65536 + 32768 * 25, 65536 + 32768 * 26, 65536 + 32768 * 27, 65536 + 32768 * 28, 65536 + 32768 * 29, 65536 + 32768 * 30
    };
    public static Map<Class<?>, Queue<BufferWrapper>[]> bufferQueueMap = new HashMap<Class<?>, Queue<BufferWrapper>[]>();
    public static Map<Class<?>, BufferFactory> bufferFactories = new HashMap<Class<?>, BufferFactory>();
    public static long expiredTimeInMills = 30 * 60 * 1000;
    public static Thread timeoutRecycler = new Thread(new TimeoutRecycler());
    public static AtomicLong totalNew = new AtomicLong();
    public static AtomicLong totalNewOutbound = new AtomicLong();
    public static AtomicLong totalReused = new AtomicLong();
    public static AtomicLong totalRecycled = new AtomicLong();
    public static ScheduledThreadPoolExecutor scheduledExecutor = new ScheduledThreadPoolExecutor(1);
    public static long parkNanos = TimeUnit.MILLISECONDS.toNanos(5000);

    static {
        Arrays.sort(chunkSizes);

        BufferFactory factory = new ByteBufferFactory();
        bufferFactories.put(factory.getBufferClass(), factory);
        factory = new CharBufferFactory();
        bufferFactories.put(factory.getBufferClass(), factory);
        factory = new DoubleBufferFactory();
        bufferFactories.put(factory.getBufferClass(), factory);
        factory = new FloatBufferFactory();
        bufferFactories.put(factory.getBufferClass(), factory);
        factory = new IntBufferFactory();
        bufferFactories.put(factory.getBufferClass(), factory);
        factory = new LongBufferFactory();
        bufferFactories.put(factory.getBufferClass(), factory);
        factory = new ShortBufferFactory();
        bufferFactories.put(factory.getBufferClass(), factory);
        factory = new ObjectBufferFactory();
        bufferFactories.put(factory.getBufferClass(), factory);

        for (Map.Entry<Class<?>, BufferFactory> entry : bufferFactories.entrySet()) {
            Queue<BufferWrapper>[] queueChunks = new Queue[chunkSizes.length];
            for (int i = 0; i < queueChunks.length; i++) {
                queueChunks[i] = new ConcurrentLinkedQueue<BufferWrapper>();
            }
            bufferQueueMap.put(entry.getKey(), queueChunks);
        }

        timeoutRecycler.start();
    }

    public static <T> T allocate(Class<T> bufferClz, int len) {
        return allocate(bufferClz, len, false);
    }

    public static <T> T allocate(Class<T> bufferClz, int len, boolean direct) {
        if (len <= 0) {
            throw new IllegalArgumentException("len param should be a positive integer.");
        }
        BufferFactory factory = bufferFactories.get(bufferClz);
        if (factory == null) {
            throw new IllegalArgumentException("unsupport buffer type.");
        }

        int idx = findChunkIndex(len);
        if (idx < 0) {
            Object buf = factory.allocate(len, direct);
            totalNewOutbound.incrementAndGet();
            return (T) buf;
        }

        Queue<BufferWrapper>[] queueChunks = bufferQueueMap.get(bufferClz);
        Queue<BufferWrapper> bufs = queueChunks[idx];
        Object buf = null;
        synchronized (bufs) {
            if (bufs.size() > 0) {
                Iterator<BufferWrapper> iter = bufs.iterator();
                BufferWrapper wrapper = iter.next();
                iter.remove();
                buf = wrapper.getBuffer();
            }
        }

        if (buf == null) {
            buf = factory.allocate(chunkSizes[idx], direct);
            totalNew.incrementAndGet();
        } else {
            totalReused.incrementAndGet();
        }

        if (ObjectBuffer.class.equals(buf.getClass())) {
            ((ByteBuffer) buf).position(0);
            ((ByteBuffer) buf).limit(len);
        } else {
            ((Buffer) buf).position(0);
            ((Buffer) buf).limit(len);
        }

        return (T) buf;
    }

    public static <T> T tryAllocate(T bufferClz, int len) {
        if (len <= 0) {
            throw new IllegalArgumentException("len param should be a positive integer.");
        }
        BufferFactory factory = bufferFactories.get(bufferClz);
        if (factory == null) {
            throw new IllegalArgumentException("unsupport buffer type.");
        }

        int idx = findChunkIndex(len);
        if (idx < 0) {
            return null;
        }

        Queue<BufferWrapper>[] queueChunks = bufferQueueMap.get(bufferClz);
        Queue<BufferWrapper> bufs = queueChunks[idx];
        BufferWrapper wrapper = bufs.poll();
        if (wrapper == null) {
            return null;
        }

        Object buf = wrapper.getBuffer();

        if (ObjectBuffer.class.equals(buf.getClass())) {
            ((ByteBuffer) buf).position(0);
            ((ByteBuffer) buf).limit(len);
        } else {
            ((Buffer) buf).position(0);
            ((Buffer) buf).limit(len);
        }

        return (T) buf;
    }

    public static void recycle(Object buf) {
        recycle(buf, 0);
    }

    public static void recycle(Object[] bufs) {
        recycle(bufs, 0);
    }

    public static void recycle(final Object buf, long delayMills) {
        if (buf == null) {
            return;
        }
        if (delayMills <= 0) {
            internalRecycle(buf);
        }
        scheduledExecutor.schedule(new Runnable() {
            @Override
            public void run() {
                internalRecycle(buf);
            }
        }, delayMills, TimeUnit.MILLISECONDS);
    }

    public static void recycle(final Object[] bufs, long delayMills) {
        if (bufs == null || bufs.length <= 0) {
            return;
        }
        if (delayMills <= 0) {
            internalRecycle(bufs);
        }
        scheduledExecutor.schedule(new Runnable() {
            @Override
            public void run() {
                internalRecycle(bufs);
            }
        }, delayMills, TimeUnit.MILLISECONDS);
    }

    public static String getStatistics() {
        StringBuilder buffer = new StringBuilder();
        int bufSize = 0;

        for (Map.Entry<Class<?>, Queue<BufferWrapper>[]> entry : bufferQueueMap.entrySet()) {
            buffer.append(entry.getKey().getSimpleName()).append(":");
            Queue<BufferWrapper>[] queueChunks = entry.getValue();
            for (int i = 0; i < queueChunks.length; i++) {
                int size = queueChunks[i].size();
                if (size <= 0) {
                    continue;
                }
                buffer.append(chunkSizes[i]).append("=").append(size).append(",");
                bufSize += chunkSizes[i] * size;
            }
            buffer.append("\n");
        }

        buffer.insert(0, "BufSize=" + bufSize + ",New=" + totalNew.get() + ",NewOutbound=" + totalNewOutbound.get() + ",Reused=" + totalReused.get() + ",Recycled=" + totalRecycled.get() + "\n");

        return buffer.toString();
    }

    static void internalRecycle(Object[] bufs) {
        if (bufs == null) {
            return;
        }
        for (Object buf : bufs) {
            internalRecycle(buf);
        }
    }

    static void internalRecycle(Object buf) {
        Class<?> bufferClz = null;
        boolean isObjectBuffer = false;
        if (buf instanceof ByteBuffer) {
            bufferClz = ByteBuffer.class;
        } else if (buf instanceof CharBuffer) {
            bufferClz = CharBuffer.class;
        } else if (buf instanceof DoubleBuffer) {
            bufferClz = DoubleBuffer.class;
        } else if (buf instanceof FloatBuffer) {
            bufferClz = FloatBuffer.class;
        } else if (buf instanceof IntBuffer) {
            bufferClz = IntBuffer.class;
        } else if (buf instanceof LongBuffer) {
            bufferClz = LongBuffer.class;
        } else if (buf instanceof ShortBuffer) {
            bufferClz = ShortBuffer.class;
        } else if ((bufferClz = buf.getClass()).equals(ByteBuffer.class)) {
            isObjectBuffer = true;
        } else {
            return;
        }
        int len = isObjectBuffer ? ((ByteBuffer) buf).capacity() : ((Buffer) buf).capacity();
        int idx = findChunkIndex(len);
        if (idx < 0) {
            return;
        }
        int chunkSize = chunkSizes[idx];
        if (len < chunkSize) {
            if (idx == 0) {
                return;
            }
            idx--;
        }

        Queue<BufferWrapper> bufs = bufferQueueMap.get(bufferClz)[idx];
        if (bufs.offer(new BufferWrapper(buf))) {
            totalRecycled.incrementAndGet();
        }
    }

    static int findChunkIndex(int len) {
        if (len > chunkSizes[chunkSizes.length - 1]) {
            return -1;
        }
        int idx = Arrays.binarySearch(chunkSizes, len);
        return idx >= 0 ? idx : Math.abs(idx) - 1;
    }

    public static int getChunkSize(int len) {
        int idx = findChunkIndex(len);
        if (idx < 0) {
            return len;
        }
        return chunkSizes[idx];
    }

    static class BufferWrapper {
        Object buffer;
        long ticket;

        public BufferWrapper(Object buffer) {
            this.buffer = buffer;
            this.ticket = System.currentTimeMillis();
        }

        public <T> T getBuffer() {
            return (T) buffer;
        }

        public long getTicket() {
            return ticket;
        }
    }

    static class TimeoutRecycler implements Runnable {
        @Override
        public void run() {
            while (true) {
                long ticket = System.currentTimeMillis();
                for (Queue<BufferWrapper>[] queueChunks : bufferQueueMap.values()) {
                    for (int i = 0; i < queueChunks.length; i++) {
                        Queue<BufferWrapper> bufs = queueChunks[i];
                        if (bufs.size() <= 0) {
                            continue;
                        }
                        synchronized (bufs) {
                            Iterator<BufferWrapper> iter = bufs.iterator();
                            while (iter.hasNext()) {
                                BufferWrapper wrapper = iter.next();
                                if (ticket - wrapper.getTicket() >= expiredTimeInMills) {
                                    iter.remove();
                                } else {
                                    break;
                                }
                            }
                        }
                    }
                }
                LockSupport.parkNanos(parkNanos);
            }
        }
    }

    interface BufferFactory {
        Class<?> getBufferClass();

        <T> T allocate(int len, boolean direct);
    }

    static class ByteBufferFactory implements BufferFactory {
        @Override
        public Class<? extends Buffer> getBufferClass() {
            return ByteBuffer.class;
        }

        @Override
        public Buffer allocate(int len, boolean direct) {
            return direct ? ByteBuffer.allocateDirect(len) : ByteBuffer.allocate(len);
        }
    }

    static class CharBufferFactory implements BufferFactory {
        @Override
        public Class<? extends Buffer> getBufferClass() {
            return CharBuffer.class;
        }

        @Override
        public Buffer allocate(int len, boolean direct) {
            return CharBuffer.allocate(len);
        }
    }

    static class DoubleBufferFactory implements BufferFactory {
        @Override
        public Class<? extends Buffer> getBufferClass() {
            return DoubleBuffer.class;
        }

        @Override
        public Buffer allocate(int len, boolean direct) {
            return DoubleBuffer.allocate(len);
        }
    }

    static class FloatBufferFactory implements BufferFactory {
        @Override
        public Class<? extends Buffer> getBufferClass() {
            return FloatBuffer.class;
        }

        @Override
        public Buffer allocate(int len, boolean direct) {
            return FloatBuffer.allocate(len);
        }
    }

    static class IntBufferFactory implements BufferFactory {
        @Override
        public Class<? extends Buffer> getBufferClass() {
            return IntBuffer.class;
        }

        @Override
        public Buffer allocate(int len, boolean direct) {
            return IntBuffer.allocate(len);
        }
    }

    static class LongBufferFactory implements BufferFactory {
        @Override
        public Class<? extends Buffer> getBufferClass() {
            return LongBuffer.class;
        }

        @Override
        public Buffer allocate(int len, boolean direct) {
            return LongBuffer.allocate(len);
        }
    }

    static class ShortBufferFactory implements BufferFactory {
        @Override
        public Class<? extends Buffer> getBufferClass() {
            return ShortBuffer.class;
        }

        @Override
        public Buffer allocate(int len, boolean direct) {
            return ShortBuffer.allocate(len);
        }
    }

    static class ObjectBufferFactory implements BufferFactory {
        @Override
        public Class<?> getBufferClass() {
            return ByteBuffer.class;
        }

        @Override
        public ByteBuffer allocate(int len, boolean direct) {
            return ByteBuffer.allocate(len);
        }
    }
}
